//*****************************************************************************
// eventQueue.c
//
// Copyright 2009 by Magpie, http://www.magpieti.com
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//*****************************************************************************

#include "eventQueuePrivate.h"

// The arrays are sized for one more than the supported size to allow
// for adjustment when we get an extra event.
static eventElement_t eventArray[eventQueue_SZ+1];
static eventElement_t* sortedEvents[eventQueue_SZ+1];
static int8u numberOfEvents;

// Function pointers used to inject behavior at intialization
static void (*enableEvent)(int32u eventId);
static void (*disableEvent)(int32u eventId, boolean adjacentToStart);
static eventStatusFunction sendEventStatus;
static requestScheduledEventsFunction requestScheduledEvents;

//*****************************************************************************
// initEventQueue
//
// Initializes event arrays and counts and injects functional dependencies. 
// Since the system may already be in an operational state, after 
// initialization, query for any scheduled events.
//
// In order to keep this module indendent and reusable across multiple devices 
// and also to facilitate testing, the vast majority of device specific 
// dependencies are "injected" into the module during initialization. These 
// dependencies take the form of functions supplied.
//
// enableEventFunction
//    Called at the start of an event to allow a device to perform any specific
//    actions required for the event.
// disableEventFunction
//    Called at the end of an event to allow a device to perform any specific 
//    actions required for the event. If adjacentToStart is TRUE, then another 
//    event is scheduled to start immediately after this event ends. The device 
//    should take step to ensure that spikes or gaps in operation do not occur 
//    in this situation.
// sendEventStatusFunction
//    Called to initiate a Report Event Status message.
// reqEventFunction
//    Called to notify client that up to count events that occur after
//    effective time may be sent to our device. This is required because we
//    have a limited queue size.
//
//*****************************************************************************
void initEventQueue(void (*enableEventFunction)(int32u eventId),
                    void (*disableEventFunction)(int32u eventId,
                          boolean adjacentToStart),
                    eventStatusFunction sendEventStatusFunction,
                    requestScheduledEventsFunction reqEventFunction)
{
  LOG_TRACE("initEventQueue\r\n");

  enableEvent = enableEventFunction;
  disableEvent = disableEventFunction;
  sendEventStatus = sendEventStatusFunction;
  requestScheduledEvents = reqEventFunction;

  int8u ii;
  for (ii=0; ii<eventQueue_SZ+1; ii++)
  {
    eventElement_t* event = &(eventArray[ii]);
    sortedEvents[ii] = event;
    initializeEvent(event);
  }

  numberOfEvents = 0;
  
  requestScheduledEvents(0, eventQueue_SZ);
}

//*****************************************************************************
// initializeEvent
//
// Sets event to a known state and marks as unused
//
//*****************************************************************************
void initializeEvent(eventElement_t* event)
{
  event->eventId = 0;
  event->startTime = 0;
  event->effectiveStartTime = 0;
  event->endTime = 0;
  event->effectiveEndTime = 0;
  event->duration = 0;
  event->superseded = FALSE;
  event->adjacentToNextEvent = FALSE;
  event->used = FALSE;
  event->active = FALSE;
}

//*****************************************************************************
// addNewEventToQueue
//
// Performs input validation on the request and queues the event for execution.
//
// Validation of event includes:
//
//    Duration is in range [1,1440]. Upper range is per Zigebee spec. If 
//    greater than this value, the event is rejected with a status 
//    EVENT_REJECTED sent to the requestor. A duration of zero, while allowed 
//    by the spec, is interpreted as an expired event, since the event is 
//    essentially complete when received. This is not per spec, but is an 
//    interpretation of the spec based on other use cases.
//
//    Start time is checked for a value of 0xffffffff. If it is equal to this 
//    value, the event is rejected with a status of EVENT_REJECTED per the spec.
//
//    If the event ID is a duplicate of a value already received, the request is
//    silently rejected per the spec.
//
//    The start time and duration are combined to validate that the event can be
//    constructed in a manner that results in a non-zero duration event with an
//    effective start time that occurs before the requested end time. Should this
//    condition not be true, the event should be flagged as expired per the spec.
//    Specific checks taken include (see spec for definitions of terms used):
//
//        1.) Duration is non-zero
//        2.) End time occurs in the future (after current time)
//        3.) Start time occurs before end time
//
//    If any of the above are not true, the event is rejected.
//
//*****************************************************************************
void addNewEventToQueue(int32u eventId,
                            int32u startTime,
                            int16u duration,
                            int32u currentTime)
{
  LOG_DEBUG("Adding event: 0x%4x 0x%4x %d 0x%4x\r\n",
             eventId, startTime, duration, currentTime);

  int8u status = 0;

  do
  {
    if (eventFromEventId(eventId) != NULL)
    {
      // No response required
      LOG_DEBUG("Duplicate event ID 0x%4x\r\n", eventId);
      break;
    }

    if (duration > 1440)
    {
      // duration is more than one day - reject event
      LOG_DEBUG("bad duration\r\n");
      status = EVENT_REJECTED;
      break;
    }

    if (startTime == 0xFFFFFFFF)
    {
      LOG_DEBUG("bad ST\r\n");
      status = EVENT_REJECTED;
      break;
    }

    if (!validStartAndEndTime(startTime, duration, currentTime))
    {
      LOG_DEBUG("expired\r\n");
      status = EVENT_REJECTED_EXPIRED;
      break;
    }    

    // Tell the requestor we got the message and everything checked out.
    status = EVENT_COMMAND_RECEIVED;

  } while (FALSE);

  if (status != 0)
  {
    sendEventStatus(eventId, status, startTime, duration);
  }

  if (status == EVENT_COMMAND_RECEIVED)
  {
    // Now queue the request
    addNewEventToQueuePrivate(startTime, duration, eventId,
                              currentTime);
  }
}

//*****************************************************************************
// validStartAndEndTime
//
// Validate that the event can be constructed in a manner that
// results in a non-zero duration event with an effective start
// time that occurs before the requested end time. Should this
// condition not be true, return FALSE.
//*****************************************************************************
boolean validStartAndEndTime(int32u startTime, int16u durationInMinutes,
                             int32u currentTime)
{
  if (durationInMinutes == 0)
  {
    // Zero time event, effectively expired
    return FALSE;
  }

  if (startTime == 0)
  {
    // start time of zero is "start now"
    startTime = currentTime;
  }

  // calculate desired end time
  int32u endTime = startTime + (((int32u)durationInMinutes)*60);
  
  if (currentTime >= endTime)
  {
    // if desired end time is in the past, we are done
    return FALSE;
  }  

  // Event is valid, so keep going
  return TRUE;
}

//*****************************************************************************
// addNewEventToQueuePrivate
//
//  Queues the event for execution. Assumes that all static input validation
//  has already occurred.
//
//*****************************************************************************
void addNewEventToQueuePrivate(int32u startTime, int16u durationInMinutes,
                               int32u eventId, int32u currentTime)
{
  LOG_TRACE("addNewEventToQueuePrivate:\r\n");

  // By design, there will always be an available event here, since
  // we sized the arrays to be one larger than the supported length
  // and at the end of this routine, if the array is full, we will
  // drop the last event, leaving room for the next time.
  eventElement_t* event = sortedEvents[numberOfEvents];
  
  // Note: Start and end time (calculated later) are "static"
  // after the event is queued. After that, only effective start
  // and end times will be adjusted.

  event->eventId = eventId;
  event->startTime = startTime;
  event->duration = durationInMinutes;
  event->used = TRUE;
  numberOfEvents++;

  if (event->startTime == 0)
  {
    event->startTime = currentTime; 
  }

  // Calculate desired end time
  event->endTime = event->startTime + (((int32u)durationInMinutes)*60);

  // Possibly adjust start time
  if (currentTime > event->startTime )
  {
    event->startTime = currentTime;
  }
  
  event->effectiveStartTime = event->startTime;
  event->effectiveEndTime = event->endTime;

  sortEvents();

  handlePossibleSupersede(event);

  // If we have filled all slots, drop the last event, since, by design,
  // we always need one slot open.
  if (numberOfEvents == (eventQueue_SZ+1))
  {
    deleteEvent(sortedEvents[eventQueue_SZ]);
  }

    return;
}

//*****************************************************************************
// getQueuedEventCount
//
// Accessor, returns how many events are currently queued.
//
//*****************************************************************************
int8u getQueuedEventCount(void)
{
  return numberOfEvents;
}

//*****************************************************************************
// getCurrentEvent
//
// Accessor, returns the event with the earliest start time.
//
// NOTE: returns NULL if queue is empty
//
//*****************************************************************************
eventElement_t* getCurrentEvent(void)
{
  if (sortedEvents[0]->used)
  {
    return sortedEvents[0];
  }
  else
  {
    return NULL;
  }
}

//*****************************************************************************
// getIndexedEvent
//
// Accessor, returns the sorted event with the specified index.
//
// NOTE: returns NULL if index is invalid or event is not used
//
//*****************************************************************************
eventElement_t* getIndexedEvent(int8u index)
{
  if ((index < eventQueue_SZ) && (sortedEvents[index]->used))
  {
    return sortedEvents[index];
  }
  else
  {
    return NULL;
  }
}

//*****************************************************************************
// activateCurrentEvent
//
// If there is at least one event, the event with the earliest start time is
// marked as active.
//
//*****************************************************************************
void activateCurrentEvent(void)
{
  if (sortedEvents[0]->used)
  {
    sortedEvents[0]->active = TRUE;
  }
}

//*****************************************************************************
// eventActive
//
//  Returns true if there is an event in progress.
//
//*****************************************************************************
boolean eventActive(void)
{
  return sortedEvents[0]->active;
}

//*****************************************************************************
// eventQueued
//
// Returns true if there is at least one event queued
//
//*****************************************************************************
boolean eventQueued(void)
{
  return sortedEvents[0]->used;
}

//*****************************************************************************
// sortEvents
//
// Sort events based upon start time (earliest first), algorithm is simple 
// bubble sort. Not the most efficient, but for the small number of events we
// handle, should not be a problem and it minimizes the code needed.
//*****************************************************************************
void sortEvents(void)
{
  int8u limit;
  
  LOG_TRACE("sortEvents: numberOfEvents = %d\r\n", numberOfEvents);

  for (limit=numberOfEvents-1; limit>0; limit--)
  {
    int8u ii;
    boolean swapped = FALSE;
    
    for (ii=0; ii<limit; ii++)
    {
      if (swapNeeded(sortedEvents[ii], sortedEvents[ii+1]))
      {
        eventElement_t* tmp = sortedEvents[ii];
        sortedEvents[ii] = sortedEvents[ii+1];
        sortedEvents[ii+1] = tmp;

        swapped = TRUE;
      }
    }
    
    if (!swapped)
    {
      break;
    }
  }
}

//*****************************************************************************
// swapNeeded
//
// Determines if two adjacent events should be swapped. Rules used:
//
// 1.) If the first event is not used and the second is, swap'm
// 2.) If both events are used and the start time of the first
//     is later than the start time of the second, swap'm
// 3.) Otherwise, no swap is needed.
//
//*****************************************************************************
boolean swapNeeded(eventElement_t* event1, eventElement_t* event2)
{
  if ((!event1->used) && (event2->used))
  {
    return TRUE;
  }
  else if ((event1->used) && (event2->used))
  {
    if (event1->startTime > event2->startTime)
    {
      return TRUE;
    }
  }

  return FALSE;
}

//*****************************************************************************
// handlePossibleSupersede
//
// This routine checks for queued events that may supersede other queued 
// events and adjusts the events when this occurs.
//
// Assumption: sortedEvents has been called prior to calling this routine, so
//             events are sorted.
//
// Note1: It is possible for a single event to overlap, and therefore supersede, 
//        multiple events. To handle this, the routine will possibly iterate 
//        multiple times.
//
// Note2: If an event superseded multiple events that are subsequently removed,
//        it might be necessary to query for new events to fill the queue.
//
// Note3: This test inherently works with a target event that has the same start
//        time as another event, regardless of the order in which they were or
//        not sorted.
//
// Here are the rules:
//
// The event, targetEvent, passed into the routine, is the most recently received
// event and the one that can supersede other events.
//
// Start with the second event in the list, so if only one event, we're done.
//
// Based upon where we are in the list, calculate the currentEvent and
// previousEvent.
//
// If the previousEvent is the targetEvent, then the targetEvent start time 
// occurs on or before the currentEvent start time, since the events have been 
// sorted prior to calling this routine. The only way for the targetEvent to 
// supersede the currentEvent is if the endTime of the targetEvent overlaps 
// the startTime of the current event. If this is true, supersede the 
// currentEvent.
//
// If the currentEvent is the targetEvent, then, by the same logic as above, the 
// previousEvent start time occurs on or before the targetEvent start time. The 
// only way for the targetEvent to supersede the previousEvent is if the endTime 
// of the previousEvent overlaps the startTime of the targetEvent. If this is 
// true, supersede the previousEvent, with one caveat...
//
// There is one edge case in which the previous event "might" be the first 
// event in the list and "might" be active. In this case, the event is not 
// removed from the list (per spec). Instead, the effective end time is 
// adjusted and it is marked as superseded. Since the algorithm used in this 
// routine is iterative, if a previousEvent is marked as superseded, we need 
// to skip it as it has already been adjusted.
//*****************************************************************************
void handlePossibleSupersede(eventElement_t* targetEvent)
{
  boolean checkForNewEventsAfterSupersede = (numberOfEvents >= eventQueue_SZ);

  boolean supersedeOccurred;

  do
  {
    supersedeOccurred = FALSE;

    int8u ii;
    for (ii=1; ii<numberOfEvents; ii++)
    {
      eventElement_t* currentEvent = sortedEvents[ii];
      eventElement_t* previousEvent = sortedEvents[ii-1];
      
      if (previousEvent == targetEvent)
      {
        if (targetEvent->endTime > currentEvent->startTime)
        {
          supersedeOccurred = TRUE;
          supersedeEvent(currentEvent, targetEvent);
        }      
      }
      else if (currentEvent == targetEvent)
      {
        if ((previousEvent->endTime > targetEvent->startTime) &&
            !previousEvent->superseded)
        {
          supersedeOccurred = TRUE;
          supersedeEvent(previousEvent, targetEvent);
        }
      }
    }
  } while (supersedeOccurred);

  if ((checkForNewEventsAfterSupersede) && (numberOfEvents < eventQueue_SZ))
  {
    // Queue was full and now there is space. Try to fill it.
    requestScheduledEvents(sortedEvents[numberOfEvents-1]->startTime+1,
        eventQueue_SZ-numberOfEvents);
  }
}

//*****************************************************************************
// supersedeEvent
//
// This routine supersedes the supplied event with the target event.
//
// If an event is not active, supersede is just reporting an event as 
// superseded and removing it from the queue
//
// If the event to supersede is active, the event is marked as superseded and 
// the effective end time of the event is set to the start time of the 
// superceding event. The fact that the event has been superseded will be
// reported when it ends.
//
//*****************************************************************************
void supersedeEvent(eventElement_t* eventToSupersede,
                    eventElement_t* targetEvent)
{
  if (eventToSupersede->active)
  {
    eventToSupersede->superseded = TRUE;
    eventToSupersede->adjacentToNextEvent = TRUE;
    eventToSupersede->effectiveEndTime = targetEvent->startTime;
  }
  else
  {
    sendStatus(eventToSupersede, EVENT_SUPERSEDED);
    deleteEvent(eventToSupersede);
  }
}

//*****************************************************************************
// deleteEvent
//
// This routine marks the supplied event as unused and sorts it to the bottom
// of the queue.
//
//*****************************************************************************
void deleteEvent(eventElement_t* event)
{
  initializeEvent(event);
  sortEvents();
  numberOfEvents--;
}

//*****************************************************************************
// eventFromEventId
//
// Returns the event corresponding to the supplied event ID. If it is not
// found, NULL is returned.
//
//*****************************************************************************
eventElement_t* eventFromEventId(int32u eventId)
{
  int8u ii;

  for (ii=0; ii<numberOfEvents; ii++)
  {
    if (sortedEvents[ii]->eventId == eventId)
    {
      return sortedEvents[ii];
    }
  }

  return NULL;
}

//*****************************************************************************
// processEvents
//
// Using the supplied time, review the event queue for any events that are 
// starting or ending. This routine should be called periodically (typically 
// once per second) for proper operation of this module.
//
//*****************************************************************************
void processEvents(int32u currentTime)
{
  if (eventQueued())
  {
    LOG_DEBUG("processEvents: 0x%4x\r\n",currentTime);
    eventElement_t* event = getCurrentEvent();
    
    if (event->active)
    {
      // Current event is active. Time to end it?
      boolean eventEnded = checkEventEnd(event, currentTime);
      
      if (eventEnded)
      {
        // The active event ended. Is it time for the next event to start?
        if (eventQueued())
        {
          event = getCurrentEvent();
          checkEventStart(event, currentTime);
        }
      }
    }
    else
    {
      // No active events. Is it time for the current event to start?
      checkEventStart(event, currentTime);
    }
  }
}

//*****************************************************************************
// checkEventEnd
//
// Checks to see if the supplied event, which is active, should be ended. If so:
//
//   1.) Call the client's disable event, noting whether an new event
//       is scheduled to start immediately
//   2.) Supply the correct status regarding how the event ended
//   3.) Delete the event
//   4.) Return TRUE, indicating an event was ended
//
//   Otherwise, return FALSE indicating no end occurred.
//*****************************************************************************
boolean checkEventEnd(eventElement_t* event, int32u currentTime)
{
  if (event->effectiveEndTime <= currentTime)
  {
    LOG_DEBUG("Ending[1/%d]: eid:0x%4x time:0x%4x\r\n",
                 numberOfEvents, 
                 event->eventId,
                 currentTime);
    dumpEvent(event);

    disableEvent(event->eventId, event->adjacentToNextEvent);
    
    if (event->superseded)
    {
      sendStatus(event, EVENT_SUPERSEDED);
    }
    else
    {
      sendStatus(event, EVENT_COMPLETED);
    }

    deleteEvent(event);
    
    return TRUE;
  }
  
  return FALSE;
}

//*****************************************************************************
// checkEventStart
//
// Checks to see if the supplied event, which is not active, should be started.
// If so:
//
//   1.) Mark the event as active
//   2.) Call the client's enable event function
//   2.) Send the EVENT_STARTED status
//
//*****************************************************************************
void checkEventStart(eventElement_t* event, int32u currentTime)
{
  if (event->effectiveStartTime <= currentTime)
  {
    event->active = TRUE;

    LOG_DEBUG("Starting[1/%d]: eid:0x%4x time:0x%4x\r\n",
                 numberOfEvents, 
                 event->eventId,
                 currentTime);
    dumpEvent(event);
    enableEvent(event->eventId);
    sendStatus(event, EVENT_STARTED);
  }
}

//*****************************************************************************
// sendStatus
//
// Send the supplied status for the supplied event.
//
//*****************************************************************************
void sendStatus(eventElement_t* event, int8u eventStatus)
{
  sendEventStatus(event->eventId, eventStatus, event->startTime,
                  event->duration);
}

//*****************************************************************************
// dumpEvent
//
// Dump the contents of the specified event.
//
//*****************************************************************************
void dumpEvent(eventElement_t *event)
{
  LOG_DEBUG("eid:0x%4x st:0x%4x est:0x%4x et:0x%4x eet:0x%4x dur:0x%2x adj:0x%2x sup:%d used:%d act:%d\r\n",
                 event->eventId, event->startTime, event->effectiveStartTime,
                 event->endTime, event->effectiveEndTime, event->duration,
                 event->adjacentToNextEvent, event->superseded, event->used,
                 event->active);
}

//*****************************************************************************
// dumpAllEvents
// 
// Dump the sorted event queue, including unused events.
//
//*****************************************************************************
void dumpAllEvents(void)
{
  LOG_DEBUG("Event Queue:\r\n");
  int8u ii;
  for (ii=0; ii<eventQueue_SZ+1; ii++)
  {
    dumpEvent(sortedEvents[ii]);
  }
}
